/*
 * (c) 2012 Leonid Bensman, lbensman.cs00@gtalumni.org
 */
using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing;
using System.Drawing.Imaging;
using System.Drawing.Drawing2D;

namespace CalcSig
{
    public class PerceptualHashAlgorithm
    {
        private int _dimensionResolution;
        public int DimensionResolution
        {
            get
            {
                return this._dimensionResolution;
            }
            private set
            {
                this._dimensionResolution = value;
            }
        }

        private int _grayDepthBits;
        public int GrayDepthBits
        {
            get
            {
                return this._grayDepthBits;
            }
            private set
            {
                this._grayDepthBits = value > 8 ? 8 : value;
            }
        }


        private PerceptualHashAlgorithm()
            : this(8, 6)
        {
        }

        private PerceptualHashAlgorithm(int dimensionResolution, int grayDepth)
        {
            this.DimensionResolution = dimensionResolution;
            this.GrayDepthBits = grayDepth;
        }

        public static PerceptualHashAlgorithm Create()
        {
            return new PerceptualHashAlgorithm();
        }

        public static PerceptualHashAlgorithm Create(int dimensionResolution, int grayDepthBits)
        {
            return new PerceptualHashAlgorithm(dimensionResolution, grayDepthBits);
        }

        public PHash AverageHash(System.Drawing.Image theImage)
        {
            // Squeeze the image down to an 8x8 6-bit grayscale image.

            // Chant the ancient incantations to create the correct data structures.
            Bitmap squeezedImage = new Bitmap(this.DimensionResolution, this.DimensionResolution, PixelFormat.Format32bppRgb);
            Graphics drawingArea = Graphics.FromImage(squeezedImage);
            drawingArea.CompositingQuality = CompositingQuality.HighQuality;
            drawingArea.InterpolationMode = InterpolationMode.HighQualityBilinear;
            drawingArea.SmoothingMode = SmoothingMode.HighQuality;
            drawingArea.DrawImage(theImage, 0, 0, this.DimensionResolution, this.DimensionResolution);
            byte[] grayScaleImage = new byte[this.DimensionResolution * this.DimensionResolution];

            int averageValue = 0;
            PHash finalHash = new PHash(this.DimensionResolution * this.DimensionResolution);

            for (int y = 0; y < this.DimensionResolution; y++)
            {
                for (int x = 0; x < this.DimensionResolution; x++)
                {
                    // Add together the red, green and blue
                    // components and reduce them to GrayDepthBits bit grayscale.
                    int pixelColour = squeezedImage.GetPixel(x, y).ToArgb();
                    int grayTone = (pixelColour & 0x00FF0000) >> 16;
                    grayTone += (pixelColour & 0x0000FF00) >> 8;
                    grayTone += (pixelColour & 0x000000FF);
                    grayTone = (int)Math.Round((decimal)grayTone / (decimal)(3 + 2 * (8 - this.GrayDepthBits)));

                    squeezedImage.SetPixel(x, y, Color.FromArgb(grayTone, grayTone, grayTone));
                    grayScaleImage[x + y * this.DimensionResolution] = (byte)grayTone;
                    averageValue += grayTone;
                }
            }
            averageValue /= this.DimensionResolution * this.DimensionResolution;

            // Reduce the colour to 6-bit grayscale and calculate the average value.
            //for (int y = 0; y < 8; y++)
            //{
            //    for (int x = 0; x < 8; x++)
            //    {
            //        // Add together the red, green and blue
            //        // components and reduce them to 6 bit grayscale.
            //        uint pixelColour = (uint)squeezedImage.GetPixel(x, y).ToArgb();
            //        uint grayTone = (pixelColour & 0x00FF0000) >> 16;
            //        grayTone += (pixelColour & 0x0000FF00) >> 8;
            //        grayTone += (pixelColour & 0x000000FF);
            //        grayTone /= 12;

            //        grayScaleImage[x + y * 8] = (byte)grayTone;
            //        averageValue += grayTone;
            //    }
            //}
            //averageValue /= 64;

            // Return 1-bits when the tone is equal to or above the average,
            // and 0-bits when it's below the average.
            for (int k = 0; k < this.DimensionResolution * this.DimensionResolution; k++)
            {
                //if (k % 8 == 0)
                //    Console.WriteLine();
                finalHash.SetBit(k, grayScaleImage[k] >= averageValue);
                //if (grayScaleImage[k] >= averageValue)
                //{
                //    //finalHash |= (1UL << ((this.DimensionResolution * this.DimensionResolution - 1) - k));
                    
                //    //Console.Write(" ");
                //}
                //else
                //Console.Write("#");
            }
            //Console.WriteLine();
            //Console.WriteLine();

            return finalHash;
        }

    }

    public class PHash
    {
        private readonly bool[] pHash;
        public PHash(int size)
        {
            this.pHash = new bool[size];
        }

        public void SetBit(int bit, bool value)
        {
            this.pHash[bit] = value;
        }

        public bool GetBit(int bit)
        {
            if (bit >= this.pHash.Length)
            {
                throw new IndexOutOfRangeException("bit parameter is greater than the bitsize of the PHash");
            }

            return this.pHash[bit];
        }

        public bool this[int index]
        {
            get { return this.GetBit(index); }
            set { this.SetBit(index, value); }
        }

        public int Length { get { return this.pHash.Length; } }

        public override string ToString()
        {
            StringBuilder sb = new StringBuilder(this.pHash.Length);

            for (int i = 0; i < this.pHash.Length; i++)
            {
                sb.Append(this.pHash[i] ? '1' : '0');
            }

            return sb.ToString();
        }

        static public bool TryParse(string input, out PHash pHash)
        {
            if (string.IsNullOrEmpty(input))
            {
                //todo find better exception
                throw new Exception("input");
            }

            pHash = new PHash(input.Length);

            for (int i = 0; i < input.Length; i++)
            {
                char bitChar = input[i];
                if (!(bitChar == '0' || bitChar == '1'))
                {
                    pHash = null;
                    return false;
                }
                pHash.SetBit(i, bitChar == '1');
            }

            return true;
        }

        static public PHash Parse(string input)
        {
            PHash pHash;
            if (!TryParse(input, out pHash))
            {
                throw new FormatException("invalid hash");
            }

            return pHash;
        }
    }
}
